'''
    Models, Meshes and Primitives.
    
    All matrices are row-major, this files makes sure that any calls involving OpenGL matrices
    pass the matrices as column-major. Rest of the application should always specify matrices in row-major.
    
    Note that models and meshes share the same argument space (**kwargs). It is therefore imperative
    that these work in harmony. Any mesh/model subclass must accept **kwargs at initialization, even if it
    unused or thrown immediately
'''

from types import Vector3, Vector4, Matrix4, Vector2, Matrix3
from math import sqrt
from vbo import VertexBuffer
from numpy import array, float32
from resources import TextureGroup
from core import *
import fudge

Epsilon = 1.0e-5


class Model(object):
    '''
        Basic Model Class
        
        A model is a 3D Object which can be displayed on the screen. 
        It includes mesh, position, rotation, scale data and textures.
        Note that mesh data, including normals and tex coords, is stored separately.
    '''   
    def __init__(self, mesh, position = None, scale = None, rotation = None, textures = None, **kwargs):
        self._Transform = Matrix4()   # Local Transformation - in column-major!
        self._Mesh = mesh
        self._Position = position or Vector3()
        rotation = rotation or Matrix3()
        self._Transform.embed(rotation.T())
        self._Scale = scale or Vector3(1.0, 1.0, 1.0)
        self._children = []
        self.parent = None
        self.textures = textures if textures else TextureGroup()
        
    def _transform(self):
        # transformation
        glTranslatef(*self._Position)
        glMultMatrixf(self._Transform._data)
        glScalef(*self._Scale)
        
    def _draw(self):
        ''' 
            Draw/Render function, do not override 
            
            This function takes care of matrix manipulation and setting textures, if there are any
        '''
        glPushMatrix()
        self._transform()
        
        self._internal_draw()
        
        for child in self._children:
            child._draw()
            
        glPopMatrix()
    
    def _internal_draw(self):
        self.textures.bindAll(0)        # TODO: automatically bound to 0, change if necessary
        self._Mesh.display()
        self.textures.unbindAll()
        
    def getAbsolutePosition(self, v = Vector3()):
        ''' 
            Gets absolute position of vector v specified in this obj's space, in World coordinates
            TODO: Doesn't do rotation
        '''
        if self.parent is None:
            return self._Position + v
        else:
            return self._Position + self.parent.getAbsolutePosition(v)
    
    def addModel(self, child):
        '''
            Adds a child model
            
            returns self
        '''
        if isinstance(child, Model):
            child.parent = self
            self._children.append(child)
        return self
    
    def getChild(self, index):
        return self._children[index]
    
    def moveTo(self, x, y, z):
        self._Position.x = x
        self._Position.y = y
        self._Position.z = z
        
    def moveToVec(self, new_position = Vector3()):
        self._Position = new_position
      
       
class Mesh(object):
    '''
        (ABC)
        
        Mesh in its simplest form is a display function.
        Subclasses must implement this.
    '''
    def display(self):
        abstract
        
class VBOMesh(Mesh):
    '''
        Mesh using a VBO for display.
        
        Asssume triangles. Assumes normals and vertices are supplied.
        Can calculate tangents at load time
    '''
    def __init__(self, vertices, normals, textureCoords = None, calc_tangents = False, **kwargs):
        # create VBOs
        self.vertices_vbo = VertexBuffer(array(flatten(vertices), dtype=float32))        
        self.normals_vbo = VertexBuffer(array(flatten(normals), dtype=float32))
        self.tex_coords_vbo = VertexBuffer(array(flatten(textureCoords), dtype=float32)) if textureCoords else None
        
        self.vertices = len(vertices)
        self.tangents = None
        
        # compute tangents
        if textureCoords is not None and calc_tangents:
            tangents = []
            vect = [Vector3()] * 3
            tex = [Vector2()] * 3
            v1 = Vector3()
            v2 = Vector3()
            t1 = Vector2()
            t2 = Vector2()
            sdir = Vector3()
            tdir = Vector3()
            i = 0
            for t in range(len(vertices) / 3):
                for v in range(3):
                    vect[v] = Vector3(*vertices[i])
                    tex[v] = Vector2(*textureCoords[i])
                    i += 1
                    
                i -= 3
                # pre compute values
                v1 = vect[1] - vect[0]
                v2 = vect[2] - vect[0]
                
                t1 = tex[1] - tex[0]
                t2 = tex[2] - tex[0]
                
                
                # calculate tangents and bitangent
                sdir = v1 * float(t2.y) - v2 * float(t1.y)
                tdir = v2 * float(t1.x) - v1 * float(t2.x)
                
                for v in range(3):
                    normal = Vector3(*normals[i])
                    tangent = (sdir - (normal * float(normal.dot(sdir))))
                    tangent.normalize()
                    tangentW = -1.0 if Vector3.Cross(normal, sdir).dot(tdir) < 0.0 else 1.0
                    i += 1
                    tangents.append([tangent.x, tangent.y, tangent.z, tangentW])
            # create VBO for this beast
            self.tangents = VertexBuffer(array(flatten(tangents), dtype=float32))
        
    def display(self):
        glEnableClientState(GL_VERTEX_ARRAY)
        glEnableClientState(GL_NORMAL_ARRAY)
        if self.tangents:
            glEnableClientState(GL_COLOR_ARRAY) # HAX!! Using the color array for tangents
            self.tangents.bind_colors(4, GL_FLOAT, 0)
            
        if self.tex_coords_vbo:
            glEnableClientState(GL_TEXTURE_COORD_ARRAY)
            glClientActiveTexture(GL_TEXTURE0)
            self.tex_coords_vbo.bind_texcoords(2, GL_FLOAT, 0)
        
        self.vertices_vbo.bind_vertexes(3, GL_FLOAT, 0)
        self.normals_vbo.bind_normals(GL_FLOAT, 0)
        glDrawArrays(GL_TRIANGLES, 0, self.vertices)
        glDisableClientState(GL_VERTEX_ARRAY)
        glDisableClientState(GL_NORMAL_ARRAY)
        if self.tangents:
            glDisableClientState(GL_COLOR_ARRAY)
        if self.tex_coords_vbo:
            glDisableClientState(GL_TEXTURE_COORD_ARRAY)
    
#
# Primitives and Pre-defineds
#
class Empty(Model):
    '''
        Empty model, can be used as a root model or as a positioning parent
    '''
    def __init__(self, position = None, scale = None, rotation = None):
        self._Transform = Matrix4()
        self._Position = position or Vector3()
        rotation = rotation or Matrix3()
        self._Transform.embed(rotation.T())
        self._Scale = scale or Vector3(1.0, 1.0, 1.0)
        self._children = []
        self.parent = None
    
    def _internal_draw(self):
        pass

class EmptyMesh(Mesh):
    def display(self):
        pass
        
class Cube(Model):
    ''' Cube Model '''
    class Mesh(Mesh):
        ''' Cube Mesh '''
        def __init__(self, size, **kwargs):
            self.size = size
        
        def display(self):
            glShadeModel(GL_FLAT)        
            glutSolidCube(self.size)
        
    def __init__(self, size, **kwargs):
        super(self.__class__, self).__init__(mesh = Cube.Mesh(size), **kwargs)
        
class Sphere(Model):
    class Mesh(Mesh):
        def __init__(self, radius, **kwargs):
            self.radius = radius
            
        def display(self):
            glShadeModel(GL_SMOOTH)
            glutSolidSphere(self.radius, 20, 20)

    def __init__(self, radius, **kwargs):
        super(self.__class__, self).__init__(mesh = Sphere.Mesh(radius), **kwargs)
        
            
class Plane(Model):
    class Mesh(Mesh):
        def __init__(self, size, **kwargs):
            self.size = size
            super(self.__class__, self).__init__(**kwargs)

            
            
        def display(self):
            ''' remember, using colors for tangents... '''
            glBegin(GL_QUADS)
            glTexCoord2f(-self.size, -self.size)
            glNormal3f(0.0, 1.0, 0.0)
            glColor4f(1.0, 0.0, 0.0, -1.0)
            glVertex3f(-self.size, 0.0, -self.size)
            glTexCoord2f(-self.size, self.size)
            glNormal3f(0.0, 1.0, 0.0)
            glColor4f(1.0, 0.0, 0.0, -1.0)
            glVertex3f(-self.size, 0.0, self.size)
            glTexCoord2f(self.size, self.size)
            glNormal3f(0.0, 1.0, 0.0)
            glColor4f(1.0, 0.0, 0.0, -1.0)
            glVertex3f(self.size, 0.0, self.size)
            glTexCoord2f(self.size, -self.size)
            glNormal3f(0.0, 1.0, 0.0)
            glColor4f(1.0, 0.0, 0.0, -1.0)
            glVertex3f(self.size, 0.0, -self.size)
            glEnd()
            super(self.__class__, self).__init__()
    
    def __init__(self, size, **kwargs):
        super(self.__class__, self).__init__(mesh = Plane.Mesh(size), **kwargs)

       
        
class Torus(Model):
    class Mesh(Mesh):
        def __init__(self, minor_radius, major_radius, **kwargs):
            self.minor = minor_radius
            self.major = major_radius
        
        def display(self):
            MinorRadius = self.minor
            MajorRadius = self.major
            glBegin( GL_TRIANGLE_STRIP );									# // Start A Triangle Strip
            for i in xrange (20): 											# // Stacks
                for j in xrange (-1, 20): 										# // Slices
                    # NOTE, python's definition of modulus for negative numbers returns
                    # results different than C's
                    #       (a / d)*d  +  a % d = a
                    if (j < 0):
                        wrapFrac = (-j%20)/20.0
                        wrapFrac *= -1.0
                    else:
                        wrapFrac = (j%20)/20.0;
                    phi = PI2*wrapFrac;
                    sinphi = sin(phi);
                    cosphi = cos(phi);

                    r = MajorRadius + MinorRadius*cosphi;

                    glNormal3f (sin(PI2*(i%20+wrapFrac)/20.0)*cosphi, sinphi, cos(PI2*(i%20+wrapFrac)/20.0)*cosphi);
                    glVertex3f (sin(PI2*(i%20+wrapFrac)/20.0)*r, MinorRadius*sinphi, cos(PI2*(i%20+wrapFrac)/20.0)*r);

                    glNormal3f (sin(PI2*(i+1%20+wrapFrac)/20.0)*cosphi, sinphi, cos(PI2*(i+1%20+wrapFrac)/20.0)*cosphi);
                    glVertex3f (sin(PI2*(i+1%20+wrapFrac)/20.0)*r, MinorRadius*sinphi, cos(PI2*(i+1%20+wrapFrac)/20.0)*r);
            glEnd()
            
    def __init__(self, minor_radius, major_radius, **kwargs):
        super(self.__class__, self).__init__(mesh = Torus.Mesh(minor_radius, major_radius), **kwargs)

class ArcBall(Model):
    class Mesh(Mesh):
        def __init__ (self, size, **kwargs):
            self.m_StVec = Vector3()
            self.m_EnVec = Vector3()
            self.m_AdjustWidth = 1.0
            self.m_AdjustHeight = 1.0
            self.size = size
            self.HACK = True
            
        def display(self):
            if self.HACK:
                self.HACK = False
                self.quadratic = gluNewQuadric();
                gluQuadricNormals(self.quadratic, GLU_SMOOTH);
                gluQuadricDrawStyle(self.quadratic, GLU_FILL); 
            gluSphere(self.quadratic,self.size,20,20)
            
        def __str__ (self):
            return "ArcBall"
            
    def __init__(self, size, **kwargs):
        super(self.__class__, self).__init__(mesh = ArcBall.Mesh(size), **kwargs)
        # retrieve current engine height/ width
        width = fudge.Engine().width
        height = fudge.Engine().height
            
        self.setBounds(width, height)
        
    def setBounds(self, NewWidth, NewHeight):
            # Set new bounds
            assert (NewWidth > 1.0 and NewHeight > 1.0), "Invalid width or height for bounds."
            # Set adjustment factor for width/height
            self.m_AdjustWidth = 1.0 / ((NewWidth - 1.0) * 0.5)
            self.m_AdjustHeight = 1.0 / ((NewHeight - 1.0) * 0.5)

    def _mapToSphere (self, NewPt):
        # Given a new window coordinate, will modify NewVec in place

        TempPt = Vector3()
        # Adjust point coords and scale down to range of [-1 ... 1]
        TempPt.x = (NewPt.x * self.m_AdjustWidth) - 1.0
        TempPt.y = 1.0 - (NewPt.y * self.m_AdjustHeight)
        # Compute the square of the length of the vector to the point from the center
        length = TempPt.dot(TempPt)
        # If the point is mapped outside of the sphere... (length > radius squared)
        if (length > 1.0):
            norm    = 1.0 / sqrt(length)

            # //Return the "normalized" vector, a point on the sphere
            TempPt.x = TempPt.x * norm
            TempPt.y = TempPt.y * norm
            TempPt.z = 0.0;
        else:			# //Else it's on the inside
            # //Return a vector to a point mapped inside the sphere sqrt(radius squared - length)
            TempPt.z = sqrt (1.0 - length)

        return TempPt

    def click (self, NewPt):
        # //Mouse down (Point2fT
        self.m_StVec = self._mapToSphere(NewPt)
        return

    def drag (self, NewPt):
        # //Mouse drag, calculate rotation (Point2fT Quat4fT)
        """ drag (Point2fT mouse_coord) -> new_quaternion_rotation_vec
        """

        self.m_EnVec = self._mapToSphere(NewPt)

        # //Compute the vector perpendicular to the begin and end vectors
        # Perp = Vector3fT ()
        Perp = Vector3.Cross(self.m_StVec, self.m_EnVec);

        NewRot = Vector4()
        # //Compute the length of the perpendicular vector
        if (Perp.length() > Epsilon):		#    //if its non-zero
            # //We're ok, so return the perpendicular vector as the transform after all
            NewRot.x = Perp.x;
            NewRot.y = Perp.y;
            NewRot.z = Perp.z;
            # //In the quaternion values, w is cosine (theta / 2), where theta is rotation angle
            NewRot.w = self.m_StVec.dot(self.m_EnVec);
        else:		#                            //if its zero
            # //The begin and end vectors coincide, so return a quaternion of zero matrix (no rotation)
            NewRot.x = NewRot.y = NewRot.z = NewRot.w = 0.0;
            
        return NewRot